Desbloqueie o poder dos Ajudantes de Gerador Assíncrono em JavaScript para criação, transformação e gerenciamento eficientes de streams. Explore exemplos práticos e casos de uso do mundo real para construir aplicações assíncronas robustas.
Ajudantes de Gerador Assíncrono em JavaScript: Dominando a Criação e Gerenciamento de Streams
A programação assíncrona em JavaScript evoluiu significativamente ao longo dos anos. Com a introdução de Geradores Assíncronos e Iteradores Assíncronos, os desenvolvedores ganharam ferramentas poderosas para lidar com fluxos de dados assíncronos. Agora, os Ajudantes de Gerador Assíncrono em JavaScript aprimoram ainda mais essas capacidades, fornecendo uma maneira mais simplificada e expressiva de criar, transformar e gerenciar fluxos de dados assíncronos. Este guia explora os fundamentos dos Ajudantes de Gerador Assíncrono, aprofunda-se em suas funcionalidades e demonstra suas aplicações práticas com exemplos claros.
Entendendo Geradores e Iteradores Assíncronos
Antes de mergulhar nos Ajudantes de Gerador Assíncrono, é crucial entender os conceitos subjacentes de Geradores Assíncronos e Iteradores Assíncronos.
Geradores Assíncronos
Um Gerador Assíncrono é uma função que pode ser pausada e retomada, produzindo valores de forma assíncrona. Ele permite que você gere uma sequência de valores ao longo do tempo, sem bloquear a thread principal. Geradores Assíncronos são definidos usando a sintaxe async function*.
Exemplo:
async function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 500)); // Simula uma operação assíncrona
yield i;
}
}
// Uso
const sequence = generateSequence(1, 5);
Iteradores Assíncronos
Um Iterador Assíncrono é um objeto que fornece um método next(), que retorna uma promessa que resolve para um objeto contendo o próximo valor na sequência e uma propriedade done indicando se a sequência foi esgotada. Iteradores Assíncronos são consumidos usando laços for await...of.
Exemplo:
async function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 500));
yield i;
}
}
async function consumeSequence() {
const sequence = generateSequence(1, 5);
for await (const value of sequence) {
console.log(value);
}
}
consumeSequence();
Apresentando os Ajudantes de Gerador Assíncrono
Os Ajudantes de Gerador Assíncrono são um conjunto de métodos que estendem a funcionalidade dos protótipos de Geradores Assíncronos. Eles fornecem maneiras convenientes de manipular fluxos de dados assíncronos, tornando o código mais legível e fácil de manter. Esses ajudantes operam de forma preguiçosa (lazy), o que significa que eles só processam os dados quando necessário, o que pode melhorar o desempenho.
Os seguintes Ajudantes de Gerador Assíncrono estão comumente disponíveis (dependendo do ambiente JavaScript e polyfills):
mapfiltertakedropflatMapreducetoArrayforEach
Exploração Detalhada dos Ajudantes de Gerador Assíncrono
1. `map()`
O ajudante map() transforma cada valor na sequência assíncrona aplicando uma função fornecida. Ele retorna um novo Gerador Assíncrono que produz os valores transformados.
Sintaxe:
asyncGenerator.map(callback)
Exemplo: Convertendo um fluxo de números para seus quadrados.
async function* generateNumbers(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 200));
yield i;
}
}
async function processNumbers() {
const numbers = generateNumbers(1, 5);
const squares = numbers.map(async (num) => {
await new Promise(resolve => setTimeout(resolve, 100)); // Simula uma operação assíncrona
return num * num;
});
for await (const square of squares) {
console.log(square);
}
}
processNumbers();
Caso de Uso Real: Imagine buscar dados de usuários de várias APIs e precisar transformar os dados em um formato consistente. map() pode ser usado para aplicar uma função de transformação a cada objeto de usuário de forma assíncrona.
async function* fetchUsersFromMultipleAPIs(apiEndpoints) {
for (const endpoint of apiEndpoints) {
const response = await fetch(endpoint);
const data = await response.json();
for (const user of data) {
yield user;
}
}
}
async function processUsers() {
const apiEndpoints = [
'https://api.example.com/users1',
'https://api.example.com/users2'
];
const users = fetchUsersFromMultipleAPIs(apiEndpoints);
const normalizedUsers = users.map(async (user) => {
// Normaliza o formato dos dados do usuário
return {
id: user.userId || user.id,
name: user.fullName || user.name,
email: user.emailAddress || user.email
};
});
for await (const normalizedUser of normalizedUsers) {
console.log(normalizedUser);
}
}
2. `filter()`
O ajudante filter() cria um novo Gerador Assíncrono que produz apenas os valores da sequência original que satisfazem uma condição fornecida. Ele permite que você inclua valores seletivamente no fluxo resultante.
Sintaxe:
asyncGenerator.filter(callback)
Exemplo: Filtrando um fluxo de números para incluir apenas números pares.
async function* generateNumbers(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 200));
yield i;
}
}
async function processNumbers() {
const numbers = generateNumbers(1, 10);
const evenNumbers = numbers.filter(async (num) => {
await new Promise(resolve => setTimeout(resolve, 100));
return num % 2 === 0;
});
for await (const evenNumber of evenNumbers) {
console.log(evenNumber);
}
}
processNumbers();
Caso de Uso Real: Processando um fluxo de entradas de log e filtrando entradas com base em seu nível de severidade. Por exemplo, processando apenas erros e avisos.
async function* readLogFile(filePath) {
// Simula a leitura de um arquivo de log linha por linha de forma assíncrona
const logEntries = [
{ timestamp: '...', level: 'INFO', message: '...' },
{ timestamp: '...', level: 'ERROR', message: '...' },
{ timestamp: '...', level: 'WARNING', message: '...' },
{ timestamp: '...', level: 'INFO', message: '...' },
{ timestamp: '...', level: 'ERROR', message: '...' }
];
for (const entry of logEntries) {
await new Promise(resolve => setTimeout(resolve, 50));
yield entry;
}
}
async function processLogs() {
const logEntries = readLogFile('path/to/log/file.log');
const errorAndWarningLogs = logEntries.filter(async (entry) => {
return entry.level === 'ERROR' || entry.level === 'WARNING';
});
for await (const log of errorAndWarningLogs) {
console.log(log);
}
}
3. `take()`
O ajudante take() cria um novo Gerador Assíncrono que produz apenas os primeiros n valores da sequência original. É útil para limitar o número de itens processados de um fluxo potencialmente infinito ou muito grande.
Sintaxe:
asyncGenerator.take(n)
Exemplo: Pegando os 3 primeiros números de um fluxo de números.
async function* generateNumbers(start) {
let i = start;
while (true) {
await new Promise(resolve => setTimeout(resolve, 200));
yield i++;
}
}
async function processNumbers() {
const numbers = generateNumbers(1);
const firstThree = numbers.take(3);
for await (const num of firstThree) {
console.log(num);
}
}
processNumbers();
Caso de Uso Real: Exibindo os 5 principais resultados de pesquisa de uma API de busca assíncrona.
async function* search(query) {
// Simula a busca de resultados de uma API
const results = [
{ title: 'Result 1', url: '...' },
{ title: 'Result 2', url: '...' },
{ title: 'Result 3', url: '...' },
{ title: 'Result 4', url: '...' },
{ title: 'Result 5', url: '...' },
{ title: 'Result 6', url: '...' }
];
for (const result of results) {
await new Promise(resolve => setTimeout(resolve, 100));
yield result;
}
}
async function displayTopSearchResults(query) {
const searchResults = search(query);
const top5Results = searchResults.take(5);
for await (const result of top5Results) {
console.log(result);
}
}
4. `drop()`
O ajudante drop() cria um novo Gerador Assíncrono que pula os primeiros n valores da sequência original e produz os valores restantes. É o oposto de take() e é útil para ignorar partes iniciais de um fluxo.
Sintaxe:
asyncGenerator.drop(n)
Exemplo: Descartando os 2 primeiros números de um fluxo de números.
async function* generateNumbers(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 200));
yield i;
}
}
async function processNumbers() {
const numbers = generateNumbers(1, 5);
const remainingNumbers = numbers.drop(2);
for await (const num of remainingNumbers) {
console.log(num);
}
}
processNumbers();
Caso de Uso Real: Paginando através de um grande conjunto de dados recuperado de uma API, pulando os resultados já exibidos.
async function* fetchData(url, pageSize, pageNumber) {
const offset = (pageNumber - 1) * pageSize;
// Simula a busca de dados com deslocamento
const data = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
{ id: 4, name: 'Item 4' },
{ id: 5, name: 'Item 5' },
{ id: 6, name: 'Item 6' },
{ id: 7, name: 'Item 7' },
{ id: 8, name: 'Item 8' }
];
const pageData = data.slice(offset, offset + pageSize);
for (const item of pageData) {
await new Promise(resolve => setTimeout(resolve, 100));
yield item;
}
}
async function displayPage(pageNumber) {
const pageSize = 3;
const allData = fetchData('api/data', pageSize, pageNumber);
const page = allData.drop((pageNumber - 1) * pageSize); // pula itens de páginas anteriores
const results = page.take(pageSize);
for await (const item of results) {
console.log(item);
}
}
// Exemplo de uso
displayPage(2);
5. `flatMap()`
O ajudante flatMap() transforma cada valor na sequência assíncrona aplicando uma função que retorna um Iterável Assíncrono. Em seguida, ele achata o Iterável Assíncrono resultante em um único Gerador Assíncrono. Isso é útil para transformar cada valor em um fluxo de valores e, em seguida, combinar esses fluxos.
Sintaxe:
asyncGenerator.flatMap(callback)
Exemplo: Transformando um fluxo de frases em um fluxo de palavras.
async function* generateSentences() {
const sentences = [
'Esta é a primeira frase.',
'Esta é a segunda frase.',
'Esta é a terceira frase.'
];
for (const sentence of sentences) {
await new Promise(resolve => setTimeout(resolve, 200));
yield sentence;
}
}
async function* stringToWords(sentence) {
const words = sentence.split(' ');
for (const word of words) {
await new Promise(resolve => setTimeout(resolve, 50));
yield word;
}
}
async function processSentences() {
const sentences = generateSentences();
const words = sentences.flatMap(async (sentence) => {
return stringToWords(sentence);
});
for await (const word of words) {
console.log(word);
}
}
processSentences();
Caso de Uso Real: Buscando comentários para várias postagens de blog e combinando-os em um único fluxo para processamento.
async function* fetchBlogPostIds() {
const blogPostIds = [1, 2, 3]; // Simula a busca de IDs de postagens de blog de uma API
for (const id of blogPostIds) {
await new Promise(resolve => setTimeout(resolve, 100));
yield id;
}
}
async function* fetchCommentsForPost(postId) {
// Simula a busca de comentários para uma postagem de blog de uma API
const comments = [
{ postId: postId, text: `Comentário 1 para post ${postId}` },
{ postId: postId, text: `Comentário 2 para post ${postId}` }
];
for (const comment of comments) {
await new Promise(resolve => setTimeout(resolve, 50));
yield comment;
}
}
async function processComments() {
const postIds = fetchBlogPostIds();
const allComments = postIds.flatMap(async (postId) => {
return fetchCommentsForPost(postId);
});
for await (const comment of allComments) {
console.log(comment);
}
}
6. `reduce()`
O ajudante reduce() aplica uma função contra um acumulador e cada valor do Gerador Assíncrono (da esquerda para a direita) para reduzi-lo a um único valor. Isso é útil para agregar dados de um fluxo assíncrono.
Sintaxe:
asyncGenerator.reduce(callback, initialValue)
Exemplo: Calculando a soma dos números em um fluxo.
async function* generateNumbers(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 200));
yield i;
}
}
async function processNumbers() {
const numbers = generateNumbers(1, 5);
const sum = await numbers.reduce(async (accumulator, num) => {
await new Promise(resolve => setTimeout(resolve, 100));
return accumulator + num;
}, 0);
console.log('Soma:', sum);
}
processNumbers();
Caso de Uso Real: Calculando o tempo médio de resposta de uma série de chamadas de API.
async function* fetchResponseTimes(apiEndpoints) {
for (const endpoint of apiEndpoints) {
const startTime = Date.now();
try {
await fetch(endpoint);
const endTime = Date.now();
const responseTime = endTime - startTime;
await new Promise(resolve => setTimeout(resolve, 50));
yield responseTime;
} catch (error) {
console.error(`Erro ao buscar ${endpoint}: ${error}`);
yield 0; // Ou trate o erro apropriadamente
}
}
}
async function calculateAverageResponseTime() {
const apiEndpoints = [
'https://api.example.com/endpoint1',
'https://api.example.com/endpoint2',
'https://api.example.com/endpoint3'
];
const responseTimes = fetchResponseTimes(apiEndpoints);
let count = 0;
const sum = await responseTimes.reduce(async (accumulator, time) => {
count++;
return accumulator + time;
}, 0);
const average = count > 0 ? sum / count : 0;
console.log(`Tempo médio de resposta: ${average} ms`);
}
7. `toArray()`
O ajudante toArray() consome o Gerador Assíncrono e retorna uma promessa que resolve para um array contendo todos os valores produzidos pelo gerador. Isso é útil quando você precisa coletar todos os valores do fluxo em um único array para processamento posterior.
Sintaxe:
asyncGenerator.toArray()
Exemplo: Coletando números de um fluxo em um array.
async function* generateNumbers(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 200));
yield i;
}
}
async function processNumbers() {
const numbers = generateNumbers(1, 5);
const numberArray = await numbers.toArray();
console.log('Array de Números:', numberArray);
}
processNumbers();
Caso de Uso Real: Coletando todos os itens de uma API paginada em um único array para filtragem ou ordenação do lado do cliente.
async function* fetchAllItems(apiEndpoint) {
let pageNumber = 1;
const pageSize = 100; // Ajuste com base nos limites de paginação da API
while (true) {
const url = `${apiEndpoint}?page=${pageNumber}&pageSize=${pageSize}`;
const response = await fetch(url);
const data = await response.json();
if (!data || data.length === 0) {
break; // Não há mais dados
}
for (const item of data) {
await new Promise(resolve => setTimeout(resolve, 50));
yield item;
}
pageNumber++;
}
}
async function processAllItems() {
const apiEndpoint = 'https://api.example.com/items';
const allItems = fetchAllItems(apiEndpoint);
const itemsArray = await allItems.toArray();
console.log(`Buscados ${itemsArray.length} itens.`);
// Processamento adicional pode ser realizado no `itemsArray`
}
8. `forEach()`
O ajudante forEach() executa uma função fornecida uma vez para cada valor no Gerador Assíncrono. Diferente de outros ajudantes, forEach() não retorna um novo Gerador Assíncrono; ele é usado para realizar efeitos colaterais em cada valor.
Sintaxe:
asyncGenerator.forEach(callback)
Exemplo: Registrando cada número de um fluxo no console.
async function* generateNumbers(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 200));
yield i;
}
}
async function processNumbers() {
const numbers = generateNumbers(1, 5);
await numbers.forEach(async (num) => {
await new Promise(resolve => setTimeout(resolve, 100));
console.log('Número:', num);
});
}
processNumbers();
Caso de Uso Real: Enviando atualizações em tempo real para uma interface de usuário à medida que os dados são processados de um fluxo.
async function* fetchRealTimeData(dataSource) {
//Simula a busca de dados em tempo real (ex: preços de ações).
const dataStream = [
{ timestamp: new Date(), price: 100 },
{ timestamp: new Date(), price: 101 },
{ timestamp: new Date(), price: 102 }
];
for (const dataPoint of dataStream) {
await new Promise(resolve => setTimeout(resolve, 500));
yield dataPoint;
}
}
async function updateUI() {
const realTimeData = fetchRealTimeData('stock-api');
await realTimeData.forEach(async (data) => {
//Simula a atualização da UI
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`Atualizando UI com dados: ${JSON.stringify(data)}`);
// O código para realmente atualizar a UI iria aqui.
});
}
Combinando Ajudantes de Gerador Assíncrono para Pipelines de Dados Complexos
O verdadeiro poder dos Ajudantes de Gerador Assíncrono vem de sua capacidade de serem encadeados para criar pipelines de dados complexos. Isso permite que você realize múltiplas transformações e operações em um fluxo assíncrono de forma concisa e legível.
Exemplo: Filtrando um fluxo de números para incluir apenas números pares, depois elevando-os ao quadrado e, finalmente, pegando os 3 primeiros resultados.
async function* generateNumbers(start) {
let i = start;
while (true) {
await new Promise(resolve => setTimeout(resolve, 100));
yield i++;
}
}
async function processNumbers() {
const numbers = generateNumbers(1);
const processedNumbers = numbers
.filter(async (num) => num % 2 === 0)
.map(async (num) => num * num)
.take(3);
for await (const num of processedNumbers) {
console.log(num);
}
}
processNumbers();
Caso de Uso Real: Buscando dados de usuários, filtrando usuários com base em sua localização, transformando seus dados para incluir apenas campos relevantes e, em seguida, exibindo os 10 primeiros usuários em um mapa.
async function* fetchUsers() {
// Simula a busca de usuários de um banco de dados ou API
const users = [
{ id: 1, name: 'John Doe', location: 'New York', email: 'john.doe@example.com' },
{ id: 2, name: 'Jane Smith', location: 'London', email: 'jane.smith@example.com' },
{ id: 3, name: 'Ken Tan', location: 'Singapore', email: 'ken.tan@example.com' },
{ id: 4, name: 'Alice Jones', location: 'New York', email: 'alice.jones@example.com' },
{ id: 5, name: 'Bob Williams', location: 'London', email: 'bob.williams@example.com' },
{ id: 6, name: 'Siti Rahman', location: 'Singapore', email: 'siti.rahman@example.com' },
{ id: 7, name: 'Ahmed Khan', location: 'Dubai', email: 'ahmed.khan@example.com' },
{ id: 8, name: 'Maria Garcia', location: 'Madrid', email: 'maria.garcia@example.com' },
{ id: 9, name: 'Li Wei', location: 'Shanghai', email: 'li.wei@example.com' },
{ id: 10, name: 'Hans Müller', location: 'Berlin', email: 'hans.muller@example.com' },
{ id: 11, name: 'Emily Chen', location: 'Sydney', email: 'emily.chen@example.com' }
];
for (const user of users) {
await new Promise(resolve => setTimeout(resolve, 50));
yield user;
}
}
async function displayUsersOnMap(location, maxUsers) {
const users = fetchUsers();
const usersForMap = users
.filter(async (user) => user.location === location)
.map(async (user) => ({
id: user.id,
name: user.name,
location: user.location
}))
.take(maxUsers);
console.log(`Exibindo até ${maxUsers} usuários de ${location} no mapa:`);
for await (const user of usersForMap) {
console.log(user);
}
}
// Exemplos de uso:
displayUsersOnMap('New York', 2);
displayUsersOnMap('London', 5);
Polyfills e Suporte de Navegadores
O suporte para Ajudantes de Gerador Assíncrono pode variar dependendo do ambiente JavaScript. Se você precisa dar suporte a navegadores ou ambientes mais antigos, pode ser necessário usar polyfills. Um polyfill fornece a funcionalidade ausente implementando-a em JavaScript. Várias bibliotecas de polyfill estão disponíveis para Ajudantes de Gerador Assíncrono, como core-js.
Exemplo usando core-js:
// Importa os polyfills necessários
require('core-js/features/async-iterator/map');
require('core-js/features/async-iterator/filter');
// ... importe outros ajudantes necessários
Tratamento de Erros
Ao trabalhar com operações assíncronas, é crucial tratar os erros adequadamente. Com os Ajudantes de Gerador Assíncrono, o tratamento de erros pode ser feito usando blocos try...catch dentro das funções assíncronas usadas nos ajudantes.
Exemplo: Tratando erros ao buscar dados dentro de uma operação map().
async function* fetchData(urls) {
for (const url of urls) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Erro HTTP! status: ${response.status}`);
}
const data = await response.json();
yield data;
} catch (error) {
console.error(`Erro ao buscar dados de ${url}: ${error}`);
yield null; // Ou trate o erro apropriadamente, ex: produzindo um objeto de erro
}
}
}
async function processData() {
const urls = [
'https://api.example.com/data1',
'https://api.example.com/data2',
'https://api.example.com/data3'
];
const dataStream = fetchData(urls);
const processedData = dataStream.map(async (data) => {
if (data === null) {
return null; // Propaga o erro
}
// Processa os dados
return data;
});
for await (const item of processedData) {
if (item === null) {
console.log('Pulando item devido a erro');
continue;
}
console.log('Item Processado:', item);
}
}
processData();
Melhores Práticas e Considerações
- Avaliação Preguiçosa (Lazy Evaluation): Os Ajudantes de Gerador Assíncrono são avaliados de forma preguiçosa, o que significa que eles só processam dados quando são solicitados. Isso pode melhorar o desempenho, especialmente ao lidar com grandes conjuntos de dados.
- Tratamento de Erros: Sempre trate os erros adequadamente dentro das funções assíncronas usadas nos ajudantes.
- Polyfills: Use polyfills quando necessário para dar suporte a navegadores ou ambientes mais antigos.
- Legibilidade: Use nomes de variáveis descritivos e comentários para tornar seu código mais legível e fácil de manter.
- Desempenho: Esteja ciente das implicações de desempenho de encadear múltiplos ajudantes. Embora a preguiça ajude, o encadeamento excessivo ainda pode introduzir sobrecarga.
Conclusão
Os Ajudantes de Gerador Assíncrono em JavaScript fornecem uma maneira poderosa e elegante de criar, transformar e gerenciar fluxos de dados assíncronos. Ao aproveitar esses ajudantes, os desenvolvedores podem escrever um código mais conciso, legível e fácil de manter para lidar com operações assíncronas complexas. Entender os fundamentos de Geradores e Iteradores Assíncronos, juntamente com as funcionalidades de cada ajudante, é essencial para utilizar efetivamente essas ferramentas em aplicações do mundo real. Seja construindo pipelines de dados, processando dados em tempo real ou lidando com respostas de API assíncronas, os Ajudantes de Gerador Assíncrono podem simplificar significativamente seu código e melhorar sua eficiência geral.